using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Oops.Xna.Framework.Input;

namespace Oops.Xna.Framework.Viewpoint.Cameras
{
	/// <summary>
	/// Represents a camera with free look capabilities.
	/// </summary>
	public class FreeLookCamera : Camera
	{
		private IInputService inputService;
#if !XBOX
		private bool isMouseVisible;
#endif
		private Vector2[] mouseSmoothingCache = new Vector2[10];

		/// <summary>
		/// Initializes a new instance of the <see cref="FreeLookCamera"/> class.
		/// </summary>
		/// <param name="game">The associated <see cref="Microsoft.Xna.Framework.Game"/>.</param>
		/// <param name="name">The name of the <see cref="FreeLookCamera"/>.</param>
		public FreeLookCamera(Game game, string name) : base(game, name) { }

		/// <summary>
		/// Gets the <see cref="IInputService"/> in context.
		/// </summary>
		protected IInputService InputService
		{
			get { return this.inputService; }
		}

		#region IGameComponent Members

		public override void Initialize()
		{
			// Get the input service.
			this.inputService = this.Game.GetService<IInputService>();
			
			// Call inherited method.
			base.Initialize();
#if !XBOX
			// Initialize the mouse position.
			this.inputService.SetMousePosition(this.Game.GraphicsDevice.PresentationParameters.BackBufferWidth >> 1, this.Game.GraphicsDevice.PresentationParameters.BackBufferHeight >> 1);
			for (int i = 0; i < this.mouseSmoothingCache.Length; i++)
			{
				this.mouseSmoothingCache[i] = new Vector2(this.Game.GraphicsDevice.PresentationParameters.BackBufferWidth >> 1, this.Game.GraphicsDevice.PresentationParameters.BackBufferHeight >> 1);
			}
#endif
		}
		#endregion

		#region IUpdateable Members

		public override void Update(GameTime gameTime)
		{
			// Get elapsed time.
			float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

			// Get the yaw, pitch and roll of the camera's orientation.
			float yaw, pitch, roll;
#if REFERENCE
			MatrixExtension.Decompose(ref this.Orientation, out yaw, out pitch, out roll);
#else
			this.Orientation.Decompose(out yaw, out pitch, out roll);
#endif
			// The right thumb stick controls yaw and pitch.
			if (this.InputService.IsGamePadButtonDown(0, Buttons.LeftShoulder))
			{
				yaw -= this.InputService.GetGamePadRightThumbStick(0).X * elapsed * 10.0f;
				pitch -= this.InputService.GetGamePadRightThumbStick(0).Y * elapsed;
			}
#if !XBOX
			if (this.InputService.IsMouseButtonPressed(MouseButtons.Right))
			{
				// Ensure the mouse cursor is locked to the center of the screen.
				this.inputService.SetMousePosition(this.Game.GraphicsDevice.PresentationParameters.BackBufferWidth >> 1, this.Game.GraphicsDevice.PresentationParameters.BackBufferHeight >> 1);

				// Ensure the mouse cursor is invisible.
				this.isMouseVisible = this.Game.IsMouseVisible;
				this.Game.IsMouseVisible = false;
			}
			else if (this.InputService.IsMouseButtonDown(MouseButtons.Right))
			{
				// Get the filtered mouse position.
				Vector2 position = this.PerformMouseFiltering(this.inputService.MouseX, this.inputService.MouseY);

				// The mouse controls yaw and pitch.
				yaw += ((this.Game.GraphicsDevice.PresentationParameters.BackBufferWidth >> 1) - position.X) * elapsed;
				pitch += ((this.Game.GraphicsDevice.PresentationParameters.BackBufferHeight >> 1) - position.Y) * elapsed;

				// Re-position the mouse cursor to the center of the screen.
				this.inputService.SetMousePosition(this.Game.GraphicsDevice.PresentationParameters.BackBufferWidth >> 1, this.Game.GraphicsDevice.PresentationParameters.BackBufferHeight >> 1);
			}
			else if (this.InputService.IsMouseButtonReleased(MouseButtons.Right))
			{
				// Reset the mouse cursor's visibility.
				this.Game.IsMouseVisible = this.isMouseVisible;
			}
#endif
			// Update the orientation.
#if REFERENCE
			Matrix.CreateFromYawPitchRoll(yaw, pitch, roll, out this.Orientation);
#else
			this.Orientation = Matrix.CreateFromYawPitchRoll(yaw, pitch, roll);
#endif
			// Forward movement is controlled by the left thumb stick and the W/S keys.
			bool leftShoulder = this.InputService.IsGamePadButtonDown(0, Buttons.LeftShoulder);

			float step = elapsed * 40.0f;
			float forward = 0.0f;
			if (leftShoulder)
			{
				forward = -this.InputService.GetGamePadLeftThumbStick(0).Y * step;
			}
			if (this.inputService.IsKeyDown(Keys.W))
			{
				forward -= step;
			}
			if (this.inputService.IsKeyDown(Keys.S))
			{
				forward += step;
			}

			// Side movement is controlled by the left thumb stick and the A/D keys.
			float right = 0.0f;
			if (leftShoulder)
			{
				right = this.InputService.GetGamePadLeftThumbStick(0).X * step;
			}
			if (this.inputService.IsKeyDown(Keys.A))
			{
				right -= step;
			}
			if (this.inputService.IsKeyDown(Keys.D))
			{
				right += step;
			}

			// Up movement is controlled by triggers and the X/C keys.
			float up = 0.0f;
			if (leftShoulder)
			{
				up = (this.InputService.GetGamePadRightTrigger(0) - this.InputService.GetGamePadLeftTrigger(0)) * elapsed * 40.0f;
			}
			if (this.inputService.IsKeyDown(Keys.X))
			{
				up -= step;
			}
			if (this.inputService.IsKeyDown(Keys.C))
			{
				up += step;
			}

			// Adjust the position.
#if INLINE
			this.Position.X = this.Position.X + (this.Orientation.M31 * forward) + (this.Orientation.M11 * right);
			this.Position.Y = this.Position.Y + (this.Orientation.M32 * forward) + (this.Orientation.M12 * right);
			this.Position.Z = this.Position.Z + (this.Orientation.M33 * forward) + (this.Orientation.M13 * right);
#elif REFERENCE
			Vector3 xAxis, zAxis;
			MatrixExtension.GetAxis(ref this.Orientation, MatrixAxis.X, out xAxis);
			MatrixExtension.GetAxis(ref this.Orientation, MatrixAxis.Z, out zAxis);

			Vector3.Multiply(ref zAxis, forward, out zAxis);
			Vector3.Multiply(ref xAxis, right, out xAxis);

			Vector3.Add(ref this.Position, ref zAxis, out this.Position);
			Vector3.Add(ref this.Position, ref xAxis, out this.Position);
#else
			Vector3 xAxis = this.Orientation.GetAxis(MatrixAxis.X);
			Vector3 zAxis = this.Orientation.GetAxis(MatrixAxis.Z);
			this.Position += (zAxis * forward) + (xAxis * right);
#endif
			this.Position.Y += up;

			// Call inherited method.
			base.Update(gameTime);
		}

		#endregion
#if !XBOX
		private Vector2 PerformMouseFiltering(float x, float y)
		{
			for (int i = this.mouseSmoothingCache.Length - 1; i > 0; i--)
			{
				this.mouseSmoothingCache[i] = this.mouseSmoothingCache[i - 1];
			}

			this.mouseSmoothingCache[0].X = x;
			this.mouseSmoothingCache[0].Y = y;

			Vector2 average = Vector2.Zero;
			float averageTotal = 0.0f;
			float currentWeight = 1.0f;

			for (int i = 0; i < this.mouseSmoothingCache.Length; i++)
			{
#if INLINE
				average.X = average.X + (this.mouseSmoothingCache[i].X * currentWeight);
				average.Y = average.Y + (this.mouseSmoothingCache[i].Y * currentWeight);
#elif REFERENCE
				Vector2 temp;
				Vector2.Multiply(ref this.mouseSmoothingCache[i], currentWeight, out temp);
				Vector2.Add(ref average, ref temp, out average);
#else
				average += this.mouseSmoothingCache[i] * currentWeight;
#endif
				averageTotal += 1.0f * currentWeight;
				currentWeight *= 0.5f;
			}

			// Calculate the new smoothed mouse movement.
			averageTotal = 1.0f / averageTotal;
#if INLINE
			average.X = average.X * averageTotal;
			average.Y = average.Y * averageTotal;

			return average;
#elif REFERENCE
			Vector2.Multiply(ref average, averageTotal, out average);
			return average;
#else
			return average * averageTotal;
#endif
		}
#endif
	}
}
